﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Reflection;

namespace Caliburn.Micro.BikeWizard.Utils
{
    /// <summary>
    /// Listens for a specific type of event from an event publisher
    /// When that event was received a specified number of times, the SingleEventWaiter stops listening and fires the Completed event.
    /// It uses a TimeoutAction to stop waiting after a certain time, otherwise it could wait until eternity!
    /// </summary>
    public class SingleEventWaiter
    {
        #region Public Properties (Events)
        public event EventHandler<EventWaiterEventArgs> Completed = delegate { };
        public event EventHandler<EventWaiterEventArgs> TimedOut = delegate { };
        #endregion

        #region Private Members
        private TimeoutAction timeOutAction;
        private TimeSpan timeout;

        private object publisher;
        private string eventName;
        private EventInfo eventInfo;
        private Delegate eventHandler;
        private int eventRequiredFireCount;
        private int eventCurrentFireCount;
        #endregion

        #region Private Properties
        private bool isFinishedWaiting
        {
            get
            {
                return eventCurrentFireCount >= eventRequiredFireCount;
            }
        }
        #endregion

        #region Constructors
        public SingleEventWaiter(object publisher, string eventName) : this(publisher, eventName, 1)
        {
        }

        public SingleEventWaiter(object publisher, string eventName, int eventRequiredFireCount) : this(publisher, eventName, eventRequiredFireCount, new TimeSpan(0,0,10))
        {
        }

        /// <summary>
        /// Waits for the event with eventName from publisher. Stop waiting when the event has been received eventRequiredFireCount times
        /// </summary>
        /// <param name="publisher">The event publisher</param>
        /// <param name="eventName">The name of the event to subscribe to</param>
        /// <param name="eventRequiredFireCount">The number of times the event should be received before firing the Completed event</param>
        public SingleEventWaiter(object publisher, string eventName, int eventRequiredFireCount, TimeSpan timeout)
        {
            EventInfo eventInfo = publisher.GetType().GetEvent(eventName);
            if (eventInfo == null)
                throw new ArgumentException(String.Format("Event: {0} not found on object: {1}", eventName, publisher));

            this.publisher = publisher;
            this.eventName = eventName;
            this.eventInfo = eventInfo;
            this.eventRequiredFireCount = eventRequiredFireCount;
            this.eventCurrentFireCount = 0;
            this.timeout = timeout;

            MethodInfo eventHandlerMethod = this.GetType().GetMethod("Handle", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
            this.eventHandler = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, eventHandlerMethod);

            this.eventInfo.AddEventHandler(publisher, eventHandler);

            // cater for a situation where the event never fires. In this case a TimeOut event is fired
            timeOutAction = new TimeoutAction(() => FireTimedOutEvent(), timeout);
        }
        #endregion

        #region Private Methods
        private void Handle(object sender, EventArgs e)
        {
            eventCurrentFireCount++;

            if (isFinishedWaiting)
            {
                // don't forget to Cancel the timeOutAction
                if (timeOutAction != null)
                    timeOutAction.Cancel();

                // Remove the handler that was added using reflection
                this.eventInfo.RemoveEventHandler(this.publisher, this.eventHandler);
                
                // Fire Completed event
                Completed(
                    this,
                    new EventWaiterEventArgs(this.publisher, this.eventName, this.eventRequiredFireCount, this.timeout));
            }
        }

        private void FireTimedOutEvent()
        {
            // No need to manually Cancel the timeOutAction here
            // Simply fire the TimeOut event
            TimedOut(
                this,
                new EventWaiterEventArgs(this.publisher, this.eventName, this.eventRequiredFireCount, this.timeout));
        }
        #endregion
    }

    public class EventWaiterEventArgs : EventArgs
    {
        public object EventPublisher { get; private set; }
        public string EventName { get; private set; }
        public int RequiredEventCount { get; private set; }
        public TimeSpan TimeWaited { get; private set; }

        public EventWaiterEventArgs(object eventPublisher, string eventName, int requiredEventCount, TimeSpan timeWaited)
        {
            this.EventPublisher = eventPublisher;
            this.EventName = eventName;
            this.RequiredEventCount = requiredEventCount;
            this.TimeWaited = timeWaited;
        }
    }
}
